package utils

import org.joda.time.DateTime
import shapeless.{::, HList, HNil, LabelledGeneric, Lazy, Witness}
import shapeless.labelled.FieldType

/**
 * Created by yz on 25/2/2020
 */
object MyJsonTool {

  sealed trait MyJsonValue

  case class MyJsonObject(fields: List[(String, MyJsonValue)]) extends
    MyJsonValue

  case class MyJsonArray(items: List[MyJsonValue]) extends MyJsonValue

  case class MyJsonString(value: String) extends MyJsonValue

  case class MyJsonNumber(value: Double) extends MyJsonValue

  case class MyJsonBoolean(value: Boolean) extends MyJsonValue

  case object MyJsonNull extends MyJsonValue

  trait MyJsonEncoder[A] {
    def encode(value: A): MyJsonValue
  }

  object MyJsonEncoder {
    def apply[A](implicit enc: MyJsonEncoder[A]): MyJsonEncoder[A] = enc
  }

  def createEncoder[A](func: A => MyJsonValue): MyJsonEncoder[A] =
    (value: A) => func(value)

  implicit val stringEncoder: MyJsonEncoder[String] =
    createEncoder(str => MyJsonString(str))
  implicit val doubleEncoder: MyJsonEncoder[Double] =
    createEncoder(num => MyJsonNumber(num))

  implicit val intEncoder: MyJsonEncoder[Int] =
    createEncoder(num => MyJsonNumber(num))
  implicit val booleanEncoder: MyJsonEncoder[Boolean] =
    createEncoder(bool => MyJsonBoolean(bool))

  implicit def listEncoder[A]
  (implicit enc: MyJsonEncoder[A]): MyJsonEncoder[List[A]] =
    createEncoder(list => MyJsonArray(list.map(enc.encode)))

  implicit def optionEncoder[A](implicit enc: MyJsonEncoder[A]): MyJsonEncoder[Option[A]] =
    createEncoder(opt => opt.map(enc.encode).getOrElse(MyJsonNull))

  implicit def dateTimeEncoder: MyJsonEncoder[DateTime] =
    createEncoder(dateTime => MyJsonString(dateTime.toString("yyyy-MM-dd HH:mm:ss")))

  trait MyJsonObjectEncoder[A] extends MyJsonEncoder[A] {
    def encode(value: A): MyJsonObject
  }

  def createObjectEncoder[A](fn: A => MyJsonObject): MyJsonObjectEncoder[A]
  =
    (value: A) => fn(value)

  implicit val hnilEncoder: MyJsonObjectEncoder[HNil] =
    createObjectEncoder(hnil => MyJsonObject(Nil))

  implicit def hlistObjectEncoder[K <: Symbol, H, T <: HList](
                                                               implicit
                                                               witness: Witness.Aux[K],
                                                               hEncoder: Lazy[MyJsonEncoder[H]],
                                                               tEncoder: MyJsonObjectEncoder[T]
                                                             ): MyJsonObjectEncoder[FieldType[K, H] :: T] = {
    val fieldName: String = witness.value.name
    createObjectEncoder { hlist =>
      val head = hEncoder.value.encode(hlist.head)
      val tail = tEncoder.encode(hlist.tail)
      MyJsonObject((fieldName, head) :: tail.fields)
    }
  }

  implicit def genericObjectEncoder[A, H](
                                           implicit
                                           generic: LabelledGeneric.Aux[A, H],
                                           hEncoder: Lazy[MyJsonObjectEncoder[H]]
                                         ): MyJsonEncoder[A] =
    createObjectEncoder { value =>
      hEncoder.value.encode(generic.to(value))
    }

  def toJson[A](a: A)(implicit enc: MyJsonEncoder[A]) = {
    enc.encode(a)
  }

  def dealJsonValue(t: MyJsonValue): List[(String, String)] = {
    t match {
      case MyJsonObject(fields) => fields.flatMap { case (key, jsValue) =>
        val dealT = dealJsonValue(jsValue)
        dealT.map { case (k, v) =>
          val trueK = if (k.isEmpty) "" else s".${k}"
          (s"${key}${trueK}", v)
        }
      }
      case MyJsonArray(items) => List(("", items.flatMap(x => dealJsonValue(x).map(_._2)).mkString(";")))
      case MyJsonString(value) => List(("", value))
      case MyJsonNumber(value) => List(("", value.toString))
      case MyJsonBoolean(value) => List(("", value.toString))
      case MyJsonNull => List(("", ""))
    }

  }

  def getLinesByT[T](y: T)(implicit enc: MyJsonEncoder[T]) = {
    val jsObject = toJson(y)
    dealJsonValue(jsObject)
  }

}
